Conversation
…ement command Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Swarup Ghosh <swghosh@redhat.com>
📝 WalkthroughWalkthroughAdds a FastAPI server exposing GET /api-implement that validates OpenShift enhancement PR URLs, invokes the Claude Agent SDK with configured allowed tools and local plugins, streams and aggregates agent responses, logs conversations, and returns result and cost. Also adds container image build, CI workflow, and Kubernetes deployment manifests. Changes
Sequence DiagramsequenceDiagram
participant Client
participant FastAPI as FastAPI Server
participant Config as Config File
participant Agent as Claude Agent SDK
participant Plugin as Plugin Files
participant Storage as Conversation Log
Client->>FastAPI: GET /api-implement?ep_url=...&cwd=...
FastAPI->>FastAPI: Validate ep_url & cwd
FastAPI->>Config: Read allowed_tools & settings
FastAPI->>Agent: query("/oape:api-implement <ep_url>", options)
Agent->>Plugin: Load/execute plugin files
Plugin-->>Agent: AssistantMessage blocks (streamed)
Agent-->>FastAPI: AssistantMessage (stream)
FastAPI->>Storage: Append streamed messages
Agent-->>FastAPI: ResultMessage (final text + cost)
FastAPI->>Storage: Write conversation log
FastAPI-->>Client: JSON {status, ep_url, cwd, output, conversation_log, cost_usd}
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts (beta)
Comment |
Signed-off-by: Swarup Ghosh <swghosh@redhat.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@server/server.py`:
- Around line 113-117: The current broad "except Exception as exc" block
re-raises an HTTPException without chaining, which loses the original traceback;
modify the handler to either catch specific agent/SDK exceptions (e.g.,
AgentExecutionError or SDK-specific exceptions) or, if you must catch Exception,
re-raise the HTTPException using exception chaining (raise
HTTPException(status_code=500, detail=f"Agent execution failed: {exc}") from
exc) so the original traceback is preserved; update the except block that
contains "except Exception as exc" and reference HTTPException to implement this
change.
- Around line 28-30: The current open("config.json") call loads CONFIGS from a
path relative to the CWD which breaks when the server is started from the repo
root; change the file lookup in server.py to resolve the config.json path
relative to the module file (use __file__ or
pathlib.Path(__file__).resolve().parent) before opening so CONFIGS and
config_json_str always read the module-local config.json regardless of working
directory.
🧹 Nitpick comments (4)
server/server.py (3)
17-17: Unused import:anyiois imported but never used.Consider removing this import unless it's needed for an upcoming feature.
-import anyio
47-47: Remove debug print statement or use proper logging.Raw
print()statements pollute stdout and are harder to control in production. Consider removing or using Python'sloggingmodule.-print(PLUGIN_DIR) +import logging +logging.getLogger(__name__).info("Plugin directory: %s", PLUGIN_DIR)
100-104: Remove commented-out code.The commented prompt on line 102 appears to be a debug/test artifact. Consider removing it to keep the codebase clean.
async for message in query( prompt=f"/oape:api-implement {ep_url}", - # prompt="explain the enhancement proposal to me like I'm 5 in 10 sentences, {ep_url}", options=options, ):server/requirements.txt (1)
1-3: Addanyioas an explicit dependency.The
server.pyimportsanyiodirectly (line 17), but it's not listed in requirements. Whileanyiomay be installed indirectly, it is not explicitly declared in FastAPI's dependencies on PyPI, so adding it ensures the dependency is explicit and won't break if the dependency graph changes.📦 Suggested addition
fastapi>=0.115.0 uvicorn>=0.32.0 claude-agent-sdk>=0.1.0 +anyio>=4.0.0
| with open("config.json") as cf: | ||
| config_json_str = cf.read() | ||
| CONFIGS = json.loads(config_json_str) |
There was a problem hiding this comment.
Config file path is relative to CWD, not the module location.
If the server is started from a directory other than server/, this will fail to find config.json. The docstring suggests running uvicorn api.server:app which would typically be run from the repo root.
🐛 Proposed fix to resolve path relative to module
-with open("config.json") as cf:
+with open(Path(__file__).parent / "config.json") as cf:
config_json_str = cf.read()
CONFIGS = json.loads(config_json_str)🤖 Prompt for AI Agents
In `@server/server.py` around lines 28 - 30, The current open("config.json") call
loads CONFIGS from a path relative to the CWD which breaks when the server is
started from the repo root; change the file lookup in server.py to resolve the
config.json path relative to the module file (use __file__ or
pathlib.Path(__file__).resolve().parent) before opening so CONFIGS and
config_json_str always read the module-local config.json regardless of working
directory.
server/server.py
Outdated
| except Exception as exc: | ||
| raise HTTPException( | ||
| status_code=500, | ||
| detail=f"Agent execution failed: {exc}", | ||
| ) |
There was a problem hiding this comment.
Improve exception handling with proper chaining.
Per static analysis: the broad Exception catch may mask unexpected errors, and the re-raised HTTPException should chain the original exception to preserve the traceback for debugging.
🛡️ Proposed fix with exception chaining
- except Exception as exc:
- raise HTTPException(
+ except Exception as exc:
+ raise HTTPException(
status_code=500,
detail=f"Agent execution failed: {exc}",
- )
+ ) from excNote: If the Claude Agent SDK defines more specific exceptions, consider catching those instead of the broad Exception to avoid masking programming errors.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| except Exception as exc: | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Agent execution failed: {exc}", | |
| ) | |
| except Exception as exc: | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Agent execution failed: {exc}", | |
| ) from exc |
🧰 Tools
🪛 Ruff (0.15.0)
[warning] 113-113: Do not catch blind exception: Exception
(BLE001)
[warning] 114-117: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
🤖 Prompt for AI Agents
In `@server/server.py` around lines 113 - 117, The current broad "except Exception
as exc" block re-raises an HTTPException without chaining, which loses the
original traceback; modify the handler to either catch specific agent/SDK
exceptions (e.g., AgentExecutionError or SDK-specific exceptions) or, if you
must catch Exception, re-raise the HTTPException using exception chaining (raise
HTTPException(status_code=500, detail=f"Agent execution failed: {exc}") from
exc) so the original traceback is preserved; update the except block that
contains "except Exception as exc" and reference HTTPException to implement this
change.
Signed-off-by: Swarup Ghosh <swghosh@redhat.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@deploy/deployment.yaml`:
- Around line 21-55: Add a hardened securityContext to the Deployment's
container (name: oape-server) by setting runAsNonRoot: true, runAsUser to a
non-root UID, and enforcing readOnlyRootFilesystem: true while dropping all
capabilities; then provide a writable /tmp by adding a volumeMount for
mountPath: /tmp (readOnly: false) using an emptyDir volume (e.g., name: tmp-dir)
and add the corresponding volumes entry with emptyDir in the Pod spec so the
container has a writable temp directory despite a read-only root filesystem.
In `@server/server.py`:
- Around line 5-6: The usage snippet references the wrong module path; replace
"uvicorn api.server:app --reload" with the correct module path "uvicorn
server.server:app --reload" (or update to whichever importable module exposes
the FastAPI app) and ensure any other docs or comments that mention "api.server"
are updated to "server.server" so the uvicorn command imports the app correctly.
- Around line 52-58: CONVERSATION_LOG is hard-coded to /tmp which is unsafe;
make the log location configurable (e.g., read from an env var or config value)
and ensure the directory is created with private permissions before creating the
FileHandler. Update the code that defines CONVERSATION_LOG to derive the Path
from a config/env (falling back to a safe default inside the application data
dir), create the parent directory with mode 0o700 (os.makedirs(path,
exist_ok=True) + Path.chmod) and only then instantiate conv_logger, _handler and
add the FileHandler so the file is written in a non-world-writable, private
directory. Ensure conv_logger and _handler continue to be used as before.
- Around line 121-146: The messages_log entries can include objects (e.g.,
block.__dict__, message.__dict__) that are not JSON-serializable; update the
AssistantMessage and fallback branches (the block handling and the final else
handling) and the ResultMessage entry creation to sanitize content before
appending to messages_log. Implement or call a small sanitizer (e.g.,
sanitize_for_json) that recursively converts non-serializable objects to
primitives/strings (or uses json.dumps+json.loads with default=str) and apply it
to block_dict, msg_dict, and message.result (and any other content fields) so
every entry appended to messages_log is JSON-serializable; keep conv_logger
logging unchanged except use the sanitized representation for JSON output.
🧹 Nitpick comments (3)
server/server.py (1)
47-50: Avoid printing PLUGIN_DIR at import time.Line 50 prints on import, which can clutter stdout in production; prefer a logger or remove the print.
♻️ Possible tweak
- print(PLUGIN_DIR) + logging.getLogger(__name__).debug("PLUGIN_DIR=%s", PLUGIN_DIR)Dockerfile (1)
5-13: Verify Go tarball integrity.The Go toolchain is downloaded and extracted without checksum verification. Add a checksum check to avoid supply-chain risks.
🔐 Proposed hardening
+ARG GO_VERSION=1.23.6 +# Populate from the official Go checksums file. +ARG GO_SHA256=<go1.23.6.linux-amd64.tar.gz-sha256> RUN dnf install -y \ git \ make && \ - # Install Go toolchain - curl -fsSL https://go.dev/dl/go1.23.6.linux-amd64.tar.gz | tar -C /usr/local -xz && \ + # Install Go toolchain + curl -fsSL -o /tmp/go.tgz https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz && \ + echo "${GO_SHA256} /tmp/go.tgz" | sha256sum -c - && \ + tar -C /usr/local -xzf /tmp/go.tgz && rm -f /tmp/go.tgz && \deploy/deployment.yaml (1)
39-39: Pin the image to a digest (or immutable tag).Line 39 uses
:latest, which can drift and break reproducibility. Prefer a digest or an immutable tag from CI.🧩 Example
- image: ghcr.io/shiftweek/oape-ai-e2e:latest + image: ghcr.io/shiftweek/oape-ai-e2e@sha256:<digest>
| apiVersion: apps/v1 | ||
| kind: Deployment | ||
| metadata: | ||
| name: oape-server | ||
| labels: | ||
| app: oape-server | ||
| spec: | ||
| replicas: 1 | ||
| selector: | ||
| matchLabels: | ||
| app: oape-server | ||
| template: | ||
| metadata: | ||
| labels: | ||
| app: oape-server | ||
| spec: | ||
| containers: | ||
| - name: oape-server | ||
| image: ghcr.io/shiftweek/oape-ai-e2e:latest | ||
| ports: | ||
| - containerPort: 8000 | ||
| envFrom: | ||
| - configMapRef: | ||
| name: oape-server-config | ||
| env: | ||
| - name: GOOGLE_APPLICATION_CREDENTIALS | ||
| value: /secrets/gcloud/application_default_credentials.json | ||
| volumeMounts: | ||
| - name: gcloud-adc | ||
| mountPath: /secrets/gcloud | ||
| readOnly: true | ||
| volumes: | ||
| - name: gcloud-adc | ||
| secret: | ||
| secretName: gcloud-adc-secret |
There was a problem hiding this comment.
Add a securityContext and a writable /tmp mount.
The deployment lacks a hardened securityContext. If you enable a read-only root filesystem, /tmp will need an emptyDir for logging.
🔒 Proposed hardening
spec:
template:
metadata:
labels:
app: oape-server
spec:
+ securityContext:
+ runAsNonRoot: true
+ runAsUser: 1001
+ seccompProfile:
+ type: RuntimeDefault
containers:
- name: oape-server
image: ghcr.io/shiftweek/oape-ai-e2e:latest
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: true
+ capabilities:
+ drop: ["ALL"]
ports:
- containerPort: 8000
envFrom:
- configMapRef:
name: oape-server-config
env:
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /secrets/gcloud/application_default_credentials.json
volumeMounts:
- name: gcloud-adc
mountPath: /secrets/gcloud
readOnly: true
+ - name: tmp
+ mountPath: /tmp
volumes:
- name: gcloud-adc
secret:
secretName: gcloud-adc-secret
+ - name: tmp
+ emptyDir: {}🧰 Tools
🪛 Checkov (3.2.334)
[medium] 21-56: Containers should not run with allowPrivilegeEscalation
(CKV_K8S_20)
[medium] 21-56: Minimize the admission of root containers
(CKV_K8S_23)
🪛 Trivy (0.69.1)
[error] 36-49: Root file system is not read-only
Container 'oape-server' of Deployment 'oape-server' should set 'securityContext.readOnlyRootFilesystem' to true
Rule: KSV-0014
(IaC/Kubernetes)
[error] 36-49: Default security context configured
container oape-server in default namespace is using the default security context
Rule: KSV-0118
(IaC/Kubernetes)
[error] 34-53: Default security context configured
deployment oape-server in default namespace is using the default security context, which allows root privileges
Rule: KSV-0118
(IaC/Kubernetes)
🤖 Prompt for AI Agents
In `@deploy/deployment.yaml` around lines 21 - 55, Add a hardened securityContext
to the Deployment's container (name: oape-server) by setting runAsNonRoot: true,
runAsUser to a non-root UID, and enforcing readOnlyRootFilesystem: true while
dropping all capabilities; then provide a writable /tmp by adding a volumeMount
for mountPath: /tmp (readOnly: false) using an emptyDir volume (e.g., name:
tmp-dir) and add the corresponding volumes entry with emptyDir in the Pod spec
so the container has a writable temp directory despite a read-only root
filesystem.
| Usage: | ||
| uvicorn api.server:app --reload |
There was a problem hiding this comment.
Correct the module path in the usage snippet.
Line 6 points to uvicorn api.server:app, but the module path here is server.server. This will fail unless an api package exists.
🛠️ Proposed fix
- uvicorn api.server:app --reload
+ uvicorn server.server:app --reload🤖 Prompt for AI Agents
In `@server/server.py` around lines 5 - 6, The usage snippet references the wrong
module path; replace "uvicorn api.server:app --reload" with the correct module
path "uvicorn server.server:app --reload" (or update to whichever importable
module exposes the FastAPI app) and ensure any other docs or comments that
mention "api.server" are updated to "server.server" so the uvicorn command
imports the app correctly.
| CONVERSATION_LOG = Path("/tmp/conversation.log") | ||
|
|
||
| conv_logger = logging.getLogger("conversation") | ||
| conv_logger.setLevel(logging.INFO) | ||
| _handler = logging.FileHandler(CONVERSATION_LOG) | ||
| _handler.setFormatter(logging.Formatter("%(message)s")) | ||
| conv_logger.addHandler(_handler) |
There was a problem hiding this comment.
Avoid a fixed log file in /tmp.
Line 52 uses a hard-coded /tmp path, which is world-writable and can be clobbered. Make it configurable and ensure a private directory is created.
🔒 Proposed hardening
-CONVERSATION_LOG = Path("/tmp/conversation.log")
+CONVERSATION_LOG = Path(os.getenv("CONVERSATION_LOG", "/tmp/oape/conversation.log"))
+CONVERSATION_LOG.parent.mkdir(parents=True, exist_ok=True)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| CONVERSATION_LOG = Path("/tmp/conversation.log") | |
| conv_logger = logging.getLogger("conversation") | |
| conv_logger.setLevel(logging.INFO) | |
| _handler = logging.FileHandler(CONVERSATION_LOG) | |
| _handler.setFormatter(logging.Formatter("%(message)s")) | |
| conv_logger.addHandler(_handler) | |
| CONVERSATION_LOG = Path(os.getenv("CONVERSATION_LOG", "/tmp/oape/conversation.log")) | |
| CONVERSATION_LOG.parent.mkdir(parents=True, exist_ok=True) | |
| conv_logger = logging.getLogger("conversation") | |
| conv_logger.setLevel(logging.INFO) | |
| _handler = logging.FileHandler(CONVERSATION_LOG) | |
| _handler.setFormatter(logging.Formatter("%(message)s")) | |
| conv_logger.addHandler(_handler) |
🧰 Tools
🪛 Ruff (0.15.0)
[error] 52-52: Probable insecure usage of temporary file or directory: "/tmp/conversation.log"
(S108)
🤖 Prompt for AI Agents
In `@server/server.py` around lines 52 - 58, CONVERSATION_LOG is hard-coded to
/tmp which is unsafe; make the log location configurable (e.g., read from an env
var or config value) and ensure the directory is created with private
permissions before creating the FileHandler. Update the code that defines
CONVERSATION_LOG to derive the Path from a config/env (falling back to a safe
default inside the application data dir), create the parent directory with mode
0o700 (os.makedirs(path, exist_ok=True) + Path.chmod) and only then instantiate
conv_logger, _handler and add the FileHandler so the file is written in a
non-world-writable, private directory. Ensure conv_logger and _handler continue
to be used as before.
Signed-off-by: Swarup Ghosh <swghosh@redhat.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@server/server.py`:
- Line 98: The agent is hard-coded with permission_mode="bypassPermissions"
which disables permission checks; change this to read from a configurable source
(e.g., an environment variable like PERMISSION_MODE or a config setting) and
validate allowed values before passing to the agent initialization (replace the
literal "bypassPermissions" used where permission_mode is set). Default to a
restrictive mode (e.g., "enforcePermissions") when the env/config is absent,
emit a startup warning if a permissive mode like "bypassPermissions" is
selected, and add brief documentation/comments about the security implications
so production deployments use the safer default.
- Line 47: Remove the stray debug print by deleting the print(PLUGIN_DIR) call
in server.py (it runs on every import); if you need that info, replace it with a
logger.debug call instead (use the module logger or obtain one via
logging.getLogger(__name__)) so it no longer writes to stdout at import time.
🧹 Nitpick comments (1)
server/server.py (1)
58-70: Consider using POST instead of GET for this code-generation endpoint.This endpoint triggers code generation with side effects (writes to filesystem, incurs API costs). REST conventions typically reserve GET for idempotent read operations. Using POST would also allow sending the parameters in the request body, avoiding URL length limits for long paths.
| # Resolve the plugin directory (repo root) relative to this file. | ||
| # The SDK expects the path to the plugin root (containing .claude-plugin/). | ||
| PLUGIN_DIR = str(Path(__file__).resolve().parent.parent / "plugins" / "oape") | ||
| print(PLUGIN_DIR) |
There was a problem hiding this comment.
Remove debug print statement.
print(PLUGIN_DIR) will output to stdout on every module import. This should be removed or converted to a debug-level log statement.
🧹 Proposed fix
PLUGIN_DIR = str(Path(__file__).resolve().parent.parent / "plugins" / "oape")
-print(PLUGIN_DIR)
+# Optionally log at debug level:
+# logging.getLogger(__name__).debug("PLUGIN_DIR=%s", PLUGIN_DIR)🤖 Prompt for AI Agents
In `@server/server.py` at line 47, Remove the stray debug print by deleting the
print(PLUGIN_DIR) call in server.py (it runs on every import); if you need that
info, replace it with a logger.debug call instead (use the module logger or
obtain one via logging.getLogger(__name__)) so it no longer writes to stdout at
import time.
| "Execute the oape:api-implement plugin with the provided EP URL. " | ||
| ), | ||
| cwd=working_dir, | ||
| permission_mode="bypassPermissions", |
There was a problem hiding this comment.
Document or make bypassPermissions mode configurable.
Setting permission_mode="bypassPermissions" disables permission checks for the agent, which could allow unintended filesystem or tool access. Consider:
- Making this configurable via environment variable or config
- Documenting the security implications
- Using a more restrictive mode in production
🔒 Proposed fix to make permission mode configurable
options = ClaudeAgentOptions(
system_prompt=(
"You are an OpenShift operator code generation assistant. "
"Execute the oape:api-implement plugin with the provided EP URL. "
),
cwd=working_dir,
- permission_mode="bypassPermissions",
+ permission_mode=CONFIGS.get("permission_mode", "default"),
allowed_tools=CONFIGS['claude_allowed_tools'],
plugins=[{"type": "local", "path": PLUGIN_DIR}],
)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| permission_mode="bypassPermissions", | |
| options = ClaudeAgentOptions( | |
| system_prompt=( | |
| "You are an OpenShift operator code generation assistant. " | |
| "Execute the oape:api-implement plugin with the provided EP URL. " | |
| ), | |
| cwd=working_dir, | |
| permission_mode=CONFIGS.get("permission_mode", "default"), | |
| allowed_tools=CONFIGS['claude_allowed_tools'], | |
| plugins=[{"type": "local", "path": PLUGIN_DIR}], | |
| ) |
🤖 Prompt for AI Agents
In `@server/server.py` at line 98, The agent is hard-coded with
permission_mode="bypassPermissions" which disables permission checks; change
this to read from a configurable source (e.g., an environment variable like
PERMISSION_MODE or a config setting) and validate allowed values before passing
to the agent initialization (replace the literal "bypassPermissions" used where
permission_mode is set). Default to a restrictive mode (e.g.,
"enforcePermissions") when the env/config is absent, emit a startup warning if a
permissive mode like "bypassPermissions" is selected, and add brief
documentation/comments about the security implications so production deployments
use the safer default.
Programmatically call the sub-agents in one shot through a HTTP call.
Summary by CodeRabbit
New Features
Chores
CI/CD
Deployment